package de.justphil.tcg.tcgserver.commons.management;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.Response;

import org.slf4j.Logger;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.IMap;
import com.hazelcast.query.SqlPredicate;

import de.justphil.tcg.tcgserver.commons.config.GridConfig;
import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.commons.i18n.Error;
import de.justphil.tcg.tcgserver.commons.util.DateTimeUtil;
import de.justphil.tcg.tcgserver.commons.domain.Card;
import de.justphil.tcg.tcgserver.commons.domain.CardInstance;
import de.justphil.tcg.tcgserver.commons.domain.Deck;
import de.justphil.tcg.tcgserver.commons.domain.Game;
import de.justphil.tcg.tcgserver.commons.domain.Property;
import de.justphil.tcg.tcgserver.commons.domain.User;
import de.justphil.tcg.tcgserver.commons.domain.User.ManagerId;
import de.justphil.tcg.tcgserver.commons.domain.containers.NewGame;
import de.justphil.tcg.tcgserver.commons.domain.wrappers.FreebieCard;
import de.justphil.tcg.tcgserver.commons.domain.wrappers.GeoLocation;
import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;

public class DataManager {
	
	private static final Logger log = restLogger(DataManager.class);
	
	private static final DataManager instance = new DataManager();
	
	private volatile boolean init;
	
	private IMap<Long, User> users;
	private IMap<Long, Game> games;
	private IMap<Long, Long> decks; // deckId -> userId
	private IMap<Long, Card> cards;
	private IMap<Long, CardInstance> cardInstances;
	private IMap<Long, Property> properties;
	private IMap<String, NewGame> otpGames;
	
	
	
	private DataManager() { // lock constructor
		init = false;
	}
	
	private void checkInit() {
		if (!init) {
			throw new IllegalStateException("DataManager hasn't been initialized yet!");
		}
	}

	public static DataManager getInstance() {
		return instance;
	}
	
	public synchronized void init() {
		if (init) {
			throw new IllegalStateException("DataManager has already been initialized!");
		}
		
		users 			= Hazelcast.getMap(GridConfig.GRID_DB_USERS);
		games 			= Hazelcast.getMap(GridConfig.GRID_DB_GAMES);
		decks 			= Hazelcast.getMap(GridConfig.GRID_DB_DECKS);
		cards 			= Hazelcast.getMap(GridConfig.GRID_DB_CARDS);
		cardInstances 	= Hazelcast.getMap(GridConfig.GRID_DB_CARD_INSTANCES);
		properties 		= Hazelcast.getMap(GridConfig.GRID_DB_PROPERTIES);
		otpGames 		= Hazelcast.getMap(GridConfig.GRID_OTP_GAMES);
		
		init = true;
	}
	
	
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<User> getAllUsers() {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return users.values();
	}
	
	public User getUserById(long userId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		User user = users.get(userId);
		
		if (user == null) {
			throw new TCGException(Error.CLIENT_ERROR_USER_NOT_FOUND, Response.Status.NOT_FOUND);
		}
		
		return user;
	}
	
	public User getUserByUsername(String username) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Set<User> userSet = (Set<User>) users.values(new SqlPredicate("username = '" + username + "'"));
		
		if (userSet.size() > 0) {
			for (User us : userSet) {
				return us;
			}
		}
		
		return null;
	}
	
	public User getUserByIdPublicly(long userId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		User publicUser = getUserById(userId);
		
		// Hide privacy fields
		publicUser.setPassword(null);
		
		return publicUser;
	}
	
	public Collection<Game> getUserGames(long userId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		User user = getUserById( userId );
		return user.getPlayedGames();
	}
	
	public Collection<CardInstance> getUserCardInstances(long userId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		User user = getUserById( userId );
		return user.getCardInstances();
	}
	
	public Collection<Deck> getUserDecks(long userId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		User user = getUserById( userId );
		return user.getDecks();
	}
	
	public User insertUser(long userId, User user) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return users.put(userId, user);
	}
	
	public User updateUser(long userId, User user) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return users.replace(userId, user);
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<Game> getAllGames() {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return games.values();
	}
	
	public Game getGameById(long gameId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Game game = games.get(gameId);
		
		if (game == null) {
			throw new TCGException(Error.CLIENT_ERROR_GAME_NOT_FOUND, Response.Status.NOT_FOUND);
		}
		
		return game;
	}
	
	public Game insertGame(long gameId, Game game) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return games.put(gameId, game);
	}
	
	public Game updateGame(long gameId, Game game) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		
		// Set new modified date
		game.setModified(new Date(DateTimeUtil.getUTCTimeInMillis()));
		
		// Update redundant data (equal serialized Game objects) in 'users' IMap
		User otherLocationUser = null;
		Set<Game> gameList = null;
		
		for (Long userId : game.getPlayers()) {
			otherLocationUser = getUserById( userId.longValue() ); // get user out of the 'users' IMap
			
			gameList = otherLocationUser.getPlayedGames();
			
			if ( gameList.remove(game) ) {
				// 'game' is part of 'gameList'
				log.debug("Game {} is part of user's GameList (userId {}) -> Removing it!", gameId, otherLocationUser.getId());
			}
			
			log.debug("Adding game {} to user's GameList (userId {})!", gameId, otherLocationUser.getId());
			gameList.add(game);
			
			updateUser(otherLocationUser.getId(), otherLocationUser);
			
			// Update game's winner with new information
			/*
			if (u.getId() == game.getWinnerId()) {
				game.setWinnerId(otherLocationUser.getId());
			}
			*/
		}
		
		return games.replace(gameId, game);
		
	}
	
	public Game deleteGame(long gameId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Game game = getGameById(gameId);
		
		// Remove game at other locations in the cluster
		User user = null;
		for (Long userId : game.getPlayers()) {
			user = getUserById( userId.longValue() );
			
			if ( user.getPlayedGames().remove(game) ) {
				log.debug("Game {} is part of user's GameList (userId {}) -> Removing it!", gameId, userId.longValue());
				
				// Only update "users" IMap if a game has really been removed
				updateUser(userId.longValue(), user);
			}
		}
		
		return games.remove(gameId);
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<Deck> getAllDecks() throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Collection<Deck> deckColl = new HashSet<Deck>(decks.size());
		
		
		for (Long deckId : decks.keySet()) {
			for (Deck d : getUserById( decks.get(deckId) ).getDecks()) {
				if (!deckColl.contains(d)) {
					deckColl.add(d);
				}
			}
		}
		
		return deckColl;
	}
	
	public Deck getDeckById(long deckId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		if (decks.containsKey(deckId)) {
			User user = getUserById( decks.get(deckId).longValue() );
			
			if (user != null) {
				for (Deck d : user.getDecks()) {
					if (d.getId() == deckId) {
						return d;
					}
				}
			}
		}
		
		throw new TCGException(Error.CLIENT_ERROR_DECK_NOT_FOUND, Response.Status.NOT_FOUND);
		
	}
	
	public Collection<CardInstance> getDeckCardInstances(long deckId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Deck deck = getDeckById( deckId );
		return deck.getCardInstances();
	}
	
	public Long insertDeck(long deckId, Deck deck) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return decks.put(deckId, deck.getUserId());
	}
	
	public long updateDeck(long deckId, Deck deck) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return decks.replace(deckId, deck.getUserId());
	}
	
	public long deleteDeck(long deckId) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return decks.remove(deckId);
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<Card> getAllCards() {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return cards.values();
	}
	
	public Card getCardById(long cardId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Card card = cards.get(cardId);
		
		if (card == null) {
			throw new TCGException(Error.CLIENT_ERROR_CARD_NOT_FOUND, Response.Status.NOT_FOUND);
		}
		
		return card;
	}
	
	public Card getCardByName(String name) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Set<Card> cardSet = (Set<Card>) cards.values(new SqlPredicate("name = '" + name + "'"));
		
		if (cardSet.size() > 0) {
			for (Card c : cardSet) {
				return c;
			}
		}
		
		return null;
	}
	
	public Collection<CardInstance> getCardCardInstances(long cardId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Card card = getCardById(cardId);
		return card.getCardInstances();
	}
	
	public Card updateCard(long cardId, Card card) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		// User
		
		
		return cards.replace(cardId, card);
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<CardInstance> getAllCardInstances() {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return cardInstances.values();
	}
	
	public CardInstance getCardInstanceById(long ciId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		CardInstance ci = cardInstances.get(ciId);
		
		if (ci == null) {
			throw new TCGException(Error.CLIENT_ERROR_CARD_INSTANCE_NOT_FOUND, Response.Status.NOT_FOUND);
		}
		
		return ci;
	}
	
	public Collection<CardInstance> getFreebieCardInstances(GeoLocation[] region) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		GeoLocation north	= region[0];
		GeoLocation east	= region[1];
		GeoLocation south	= region[2];
		GeoLocation west	= region[3];
		
		Set<CardInstance> ciSet = (Set<CardInstance>) cardInstances.values(
				new SqlPredicate(
						"(latitude BETWEEN " + south.getLat() + " AND " + north.getLat() + ") AND " +
						"(longitude BETWEEN " + west.getLon() + " AND " + east.getLon() + ")"
				)
		);
		
		// Filter CardInstances
		
		Collection<CardInstance> ciList = new ArrayList<CardInstance>(ciSet.size());
		
		for (CardInstance ci : ciSet) {
			if (ci.getUserId() == ManagerId.LOCATION) {
				ciList.add(ci);
			}
		}
		
		return ciList;
	}
	
	public Collection<FreebieCard> getFreebieCards(GeoLocation[] region) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Collection<CardInstance> ciColl = getFreebieCardInstances(region);
		
		// Group CardInstances to FreebieCards (including quantity)
		Map<Long, FreebieCard> cardsIn = new HashMap<Long, FreebieCard>();
		
		FreebieCard alreadyIncludedCard = null;
		int q = 0;
		for (CardInstance ci : ciColl) {
				
			if ( !cardsIn.containsKey( ci.getCardId() ) ) {
				cardsIn.put(ci.getCardId(), new FreebieCard(1, getCardById(ci.getCardId())));
			}
			else {
				alreadyIncludedCard = cardsIn.get( ci.getCardId() );
				q = alreadyIncludedCard.getQuantity();
				alreadyIncludedCard.setQuantity(q+1);
			}
			
		}
		
		return cardsIn.values();
	}
	
	public CardInstance insertCardInstance(long ciId, CardInstance ci) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return cardInstances.put(ciId, ci);
	}
	
	public CardInstance updateCardInstance(long ciId, CardInstance ci) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return cardInstances.replace(ciId, ci);
	}
	
	/*
	 * This method must be synchronized before invocation since it's not thread-safe!
	 */
	public CardInstance updateFreebieCardInstanceOwnership(long ciId, CardInstance ci, User owner) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Date now = new Date(DateTimeUtil.getUTCTimeInMillis());
		
		// Update card instance
		ci.setUserId(owner.getId());
		//ci.setUser(owner);
		ci.setModified(now);
		
		CardInstance oldCi = getCardInstanceById(ciId);
		
		// Update card in cluster
		Card card = getCardById(ci.getCardId());
		
		if ( card.getCardInstances().remove( ci ) ) {
			card.getCardInstances().add(ci);
		}
		updateCard(card.getId(), card);
		
		// Update oldUser in cluster
		User oldUser = getUserById(oldCi.getUserId());
		//User oldUser = getUserById(oldCi.getUser().getId());
		if ( oldUser.getCardInstances().remove(ci) ) {
			
			for (Deck deck : oldUser.getDecks()) {
				deck.getCardInstances().remove(ci);
			}
		}
		updateUser(oldUser.getId(), oldUser);
		
		// Update new owner in cluster
		owner.getCardInstances().add(ci);
		updateUser(owner.getId(), owner);
		
		/*
		 * It is NOT necessary to update the 'decks' IMap, because the old user (tcg_manager_location)
		 * doesn't have any decks!
		 * 
		 */
		
		return cardInstances.replace(ciId, ci);
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<Property> getAllProperties() {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return properties.values();
	}
	
	public Property getPropertyById(long propertyId) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		Property p = properties.get(propertyId);
		
		if (p == null) {
			throw new TCGException(Error.CLIENT_ERROR_PROPERTY_NOT_FOUND, Response.Status.NOT_FOUND);
		}
		
		return p;
	}
	
	/* ######################################################################## */
	/* ######################################################################## */
	/* ######################################################################## */
	
	public Collection<NewGame> getAllNewGames() {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return otpGames.values();
	}
	
	public NewGame getNewGameByOTP(String otp) throws TCGException {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		NewGame newGame = otpGames.get(otp);
		
		if (newGame == null) {
			throw new TCGException(Error.CLIENT_ERROR_NEW_GAME_NOT_FOUND, Response.Status.NOT_FOUND);
		}
		
		return newGame;
	}
	
	public NewGame insertNewGame(String otp, NewGame newGame) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return otpGames.put(otp, newGame);
	}
	
	public NewGame updateNewGame(String otp, NewGame newGame) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return otpGames.replace(otp, newGame);
	}
	
	public NewGame deleteNewGame(String otp) {
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		checkInit();
		
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		return otpGames.remove(otp);
	}

}
